﻿using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace CodeAnalysisRuleCollection.Common.CSharp
{
    [ExportCodeRefactoringProvider("IntroduceFromConstructorParameter", LanguageNames.CSharp)]
    internal class IntroduceFromConstructorParameterCodeRefactoringProvider : ICodeRefactoringProvider
    {
        public async Task<IEnumerable<CodeAction>> GetRefactoringsAsync(Document document, TextSpan span, CancellationToken cancellationToken)
        {
            Contract.Requires(document != null);

            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            var diagnosticNode = root.FindToken(span.Start).Parent;

            IList<CodeAction> actions = new List<CodeAction>();

            // find the declaration identified by the diagnostic
            if ((var parameter = diagnosticNode.AncestorsAndSelf().OfType<ParameterSyntax>().FirstOrDefault()) != null)
            if ((var constructor = parameter.Parent.Parent as ConstructorDeclarationSyntax) != null)
            {
                // static constructors are not meet the case
                if (constructor.Modifiers.Any(SyntaxKind.StaticKeyword))
                    return null;

                var model = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
                IParameterSymbol symbol = model.GetDeclaredSymbol(parameter, cancellationToken) as IParameterSymbol;
                TypeDeclarationSyntax typeSyntax = constructor.Parent as TypeDeclarationSyntax;
                ITypeSymbol type = symbol.ContainingSymbol.ContainingType;

                string innerName = ToInnerName(symbol.Name),
                    propertyName = ToPascalCase(symbol.Name);

                if (!type.GetMembers(innerName).Any(s => !s.IsStatic))
                    actions.Add(CodeAction.Create("Introduce and assign read-only field", c => Fix(root, document, symbol, typeSyntax, constructor.Body, c)));

                if (!type.GetMembers(propertyName).Any(s => !s.IsStatic))
                    actions.Add(CodeAction.Create("Introduce and assign property", c => FixWithContractAsync(root, document, model, symbol, typeSyntax, constructor.Body, c)));
            }

            return actions.Any() ? actions : null;
        }

        private static string ToInnerName(string camelCasedName)
        {
            return String.Format("_{0}", camelCasedName);
        }

        private static string ToPascalCase(string camelCasedName)
        {
            return String.Concat(Char.ToUpperInvariant(camelCasedName[0]), camelCasedName.Substring(1));
        }

        private async Task<Document> Fix(
            SyntaxNode root, Document document,
            IParameterSymbol symbol, TypeDeclarationSyntax type, BlockSyntax block,
            CancellationToken cancellationToken
        )
        {
            // TODO: Find other assignments and insert into the right place

            // generate field name
            string desiredName = ToInnerName(symbol.Name);
            string fieldName = IntroducerHelper.GetNextName(symbol.ContainingType, desiredName);

            // add assignment
            var newType = type.ReplaceNode(
                block,
                block.WithStatements(
                    block.Statements.Add(
                        SyntaxFactory.ExpressionStatement(
                            SyntaxFactory.BinaryExpression(SyntaxKind.SimpleAssignmentExpression,
                                SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
                                    SyntaxFactory.ThisExpression(),
                                    SyntaxFactory.IdentifierName(fieldName)
                                )
                                    .WithAdditionalAnnotations(Simplifier.Annotation),
                                SyntaxFactory.IdentifierName(symbol.Name)
                            )
                        )
                            .WithAdditionalAnnotations(Formatter.Annotation)
                    )
                )
            );

            // add field declaration
            newType = CodeGenerator.AddFieldDeclaration(
                newType,
                CodeGenerationSymbolFactory.CreateFieldSymbol(
                    null,
                    Accessibility.Protected,
                    new SymbolModifiers(isReadOnly: true),
                    symbol.Type,
                    fieldName
                ),
                document.Project.Solution.Workspace
            );

            var newRoot = root.ReplaceNode(type, newType);

            return document.WithSyntaxRoot(newRoot);
        }

        private async Task<Document> FixWithContractAsync(
            SyntaxNode root, Document document, SemanticModel model,
            IParameterSymbol symbol, TypeDeclarationSyntax type, BlockSyntax block,
            CancellationToken cancellationToken
        )
        {
            // TODO: Find other assignments and insert into the right place

            string desiredName = ToPascalCase(symbol.Name);
            string propertyName = IntroducerHelper.GetNextName(symbol.ContainingType, desiredName);

            // add assignment
            BlockSyntax newBlock = block.WithStatements(
                block.Statements.Add(
                    SyntaxFactory.ExpressionStatement(
                        SyntaxFactory.BinaryExpression(SyntaxKind.SimpleAssignmentExpression,
                            SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
                                SyntaxFactory.ThisExpression(),
                                SyntaxFactory.IdentifierName(propertyName)
                            ),
                            SyntaxFactory.IdentifierName(symbol.Name)
                        )
                    )
                        .WithAdditionalAnnotations(Formatter.Annotation)
                )
            );

            var newType = type.ReplaceNode(
                block,
                newBlock
            );

            // add property declaration
            newType = CodeGenerator.AddPropertyDeclaration(
                newType,
                CodeGenerationSymbolFactory.CreatePropertySymbol(
                    null,
                    Accessibility.Public,
                    new SymbolModifiers(isReadOnly: true),
                    symbol.Type,
                    null,
                    propertyName,
                    null,
                    CodeGenerationSymbolFactory.CreateMethodSymbol(
                        null,
                        Accessibility.Public,
                        new SymbolModifiers(isAbstract: true),
                        symbol.Type,
                        null,
                        String.Format("get_{0}", propertyName),
                        null,
                        null
                    ),
                    CodeGenerationSymbolFactory.CreateMethodSymbol(
                        null,
                        Accessibility.Private,
                        new SymbolModifiers(isAbstract: true),
                        model.Compilation.GetSpecialType(SpecialType.System_Void),
                        null,
                        String.Format("set_{0}", propertyName),
                        null,
                        new List<IParameterSymbol>
                        {
                            CodeGenerationSymbolFactory.CreateParameterSymbol(symbol.Type, "value")
                        }
                    )
                ),
                document.Project.Solution.Workspace
            );

            var newRoot = root.ReplaceNode(type, newType);

            return document.WithSyntaxRoot(newRoot);
        }
    }
}
